/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network.util;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;

/**
 * Used to read from buffers with single bit granularity.
 */
public class BitStreamReader {
	/**
	 * Creates a bit stream reader which will read from the specified buffer.
	 *
	 * @param   buffer the buffer to read to.
	 * @param   bitPosition the position of the first readable bit in the buffer.
	 * @param   bitLimit the position of the first unreadable bit in the buffer.
	 */
	public BitStreamReader(ByteBuffer buffer, int bitPosition, int bitLimit) {
		this.buffer = buffer;

		int bytePosition = bitPosition / Byte.SIZE;
		buffer.position(bytePosition);
		bitOffset = bitPosition % Byte.SIZE;

		int byteLimit = (bitLimit + Byte.SIZE - 1) / Byte.SIZE;
		buffer.limit(byteLimit);
		unreadableEndBits = (byteLimit * Byte.SIZE) - bitLimit;
	}

	/**
	 * Reads {@code bitCount} bits from the stream and increments the position by {@code bitCount}.
	 *
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  an integer value containing the bits read from the stream, padded with 0.
	 * @throws  BufferUnderflowException if the limit would be exceeded with the read.
	 */
	public int readBits(int bitCount) {
		if (bitCount <= 0 || bitCount > 32) {
			throw new IllegalArgumentException("Invalid bit count");
		}

		if (remaining() < bitCount) {
			throw new BufferUnderflowException();
		}

		int result = 0;

		// read bits with MSB first
		int bitsRemaining = bitCount;
		// read bits in chunks
		while (bitsRemaining > 0) {
			// determine how many bits to read next
			// either read the remaining bits in the whole bitfield, or the remaining bits in the byte
			int bc = Math.min(bitsRemaining, Byte.SIZE - bitOffset);
			byte val = buffer.get(buffer.position());

			// extract the bits from the byte
			// shift over to remove "empty space" at beginning, then mask with bc 1s
			int valBits = (val >>> bitOffset) & ~(0xffffffff << bc);

			// shift and OR with result
			bitsRemaining -= bc;
			result |= (valBits << bitsRemaining);

			// increment into the stream
			bitOffset += bc;
			if (bitOffset == Byte.SIZE) {
				// we've reached the end of the byte - increment to next byte
				bitOffset = 0;
				buffer.get();
			}
		}

		return result;
	}

	/**
	 * Reads a boolean value from the stream and increments the position by 1.
	 *
	 * @return  a boolean value read from the stream.
	 * @throws  BufferUnderflowException if the limit would be exceeded with the read.
	 */
	public boolean readBoolean() {
		return readBits(1) != 0;
	}

	/**
	 * Reads an unsigned integer value of size {@code bitCount} from the stream and increments the position by
	 * {@code bitCount}. In order to ensure that the resulting value is positive, it is returned as a {@code long},
	 * but will be in the range 0 to 2<sup>32</sup> - 1.
	 *
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  a positive integer value read from the stream.
	 * @throws  BufferUnderflowException if the limit would be exceeded with the read.
	 */
	public long readUnsignedInt(int bitCount) {
		return readBits(bitCount) & 0x00000000ffffffffL;
	}

	/**
	 * Reads a signed integer value of size {@code bitCount} from the stream and increments the position by
	 * {@code bitCount}. The resulting value is sign-extended.
	 *
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  a signed integer value read from the stream.
	 * @throws  BufferUnderflowException if the limit would be exceeded with the read.
	 */
	public int readInt(int bitCount) {
		int value = readBits(bitCount);
		int signBitMask = 1 << (bitCount-1); // bitmask containing only the MSB
		if ((value & signBitMask) != 0) {
			// value is negative - need to sign extend by ORing with 1s in the remaining bits
			value |= (0xffffffff << bitCount);
		}
		return value;
	}

	/**
	 * Reads a float value from the stream and increments the position by {@link Float#SIZE}.
	 *
	 * @return  a float value read from the stream.
	 * @throws  BufferUnderflowException if the limit would be exceeded with the read.
	 */
	public float readFloat() {
		int bits = readBits(Float.SIZE);
		return Float.intBitsToFloat(bits);
	}

	/**
	 * Reads a quantized unsigned normalized float value of size {@code bitCount} from the stream and increments the
	 * position by {@code bitCount}.
	 *
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  an unsigned normalized float value read from the stream. The returned value will be in the range
	 *          {@code [0.0f, 1.0f]}.
	 * @throws  BufferUnderflowException if the limit would be exceeded with the read.
	 */
	public float readUNormFloat(int bitCount) {
		return Compressor.decompressUNormFloat((int) readUnsignedInt(bitCount), bitCount);
	}

	/**
	 * Reads a quantized signed normalized float value of size {@code bitCount} from the stream and increments the
	 * position by {@code bitCount}.
	 *
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  a signed normalized float value read from the stream. The returned value will be in the range
	 *          {@code [-1.0f, 1.0f]}.
	 * @throws  BufferUnderflowException if the limit would be exceeded with the read.
	 */
	public float readSNormFloat(int bitCount) {
		return Compressor.decompressSNormFloat((int) readUnsignedInt(bitCount), bitCount);
	}

	/**
	 * Reads a quantized wrapped normalized float value of size {@code bitCount} from the stream and increments the
	 * position by {@code bitCount}. This method differs from {@link BitStreamReader#readUNormFloat(int)} in that the
	 * range of this method is a half-open interval.
	 *
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  a wrapped normalized float value read from the stream. The returned value will be in the range
	 *          {@code [0.0f, 1.0f)}.
	 * @throws  BufferUnderflowException if the limit would be exceeded with the read.
	 */
	public float readWrappedNormFloat(int bitCount) {
		return Compressor.decompressWrappedNormFloat((int) readUnsignedInt(bitCount), bitCount);
	}

	/**
	 * Returns the limit in bits.
	 *
	 * @return  the limit in bits.
	 */
	public int limit() {
		return (buffer.limit() * Byte.SIZE) - unreadableEndBits;
	}

	/**
	 * Returns the current position in bits.
	 *
	 * @return  the current position in bits.
	 */
	public int position() {
		return (buffer.position() * Byte.SIZE) + bitOffset;
	}

	/**
	 * Returns the number of bits remaining that can be read.
	 *
	 * @return  the number of bits remaining.
	 */
	public int remaining() {
		return limit() - position();
	}

	private ByteBuffer buffer;      // underlying buffer
	private int bitOffset;          // offset into the current byte
	private int unreadableEndBits;  // number of unreadable bits in the last byte of "remaining"
}
